/*
 * Copyright (c) 2012 Sean Connelly
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
 * associated documentation files (the "Software"), to deal in the Software without restriction,
 * including without limitation the rights to use, copy, modify, merge, publish, distribute,
 * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or
 * substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
 * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

/*

This needs to track everything related to types in the program...

The current system is spread out over a couple different files, and should instead be consolidated
into one library... (this eventual file).

Some things I will need to be able to do:

Handle templating
Find a type
* by plain name ("ratio")
* by id
* by AST
Define new types (templating?!)
Handle patterned types
Track if a type is being used
Get a type id
Perhaps associate user data to the type
Hook up with other typeTracker's in order to implement import (one typeTracker per .rho file)

These are *instantiated* types... i.e., `table#[var, var]`, NOT `table`... though it should
correctly return the reference to `table#[var, var]` if the user searches for `table`.

Basically, "find" needs to look at what already exists, and generate new entries, transparently to
the user.

define bool symbol() false true end
define boolu symbol(bool) unknown end

define weekday symbol() mon tue wed thu fri end
define weeksun symbol() sun end
define weeksat symbol() sat end
define weekend symbol(weeksun, weeksat) end
define week symbol(weeksun, weekday, weeksat) end

week w ; defaults to sun

when (w)
is (weekday wd)
is (weekend we)
end

switch (w)
case (sun)
else
end


define one numcycle(100, 200) end
define two numcycle(300, 400) end
define three#[T] numcycle(T(null), 500) end

three#[one] x  ; 100 to 500
three#[two] y  ; 300 to 500

*/

var config = require('./config.js');
var dynTree = require('./dyn-tree.js');
var ratio = require('./ratio.js');
var preDefinedPatternedTypes = require('./pre-defined-patterned-types.js').add;

exports.create = function(fileInfo) {
		var statics = dynTree.create();
		var definitions = {};
		var definst = [];
		return {
				_stRatio: function() { // return the type of 'ratio'
						return statics.to('ratio').getData();
					},
				_binaryOpType: function(leftMT, op, rightMT) { // return the type of result of the binary op
						var leftNumInt = leftMT == 'numcycle' || leftMT == 'numbound';
						var rightNumInt = rightMT == 'numcycle' || rightMT == 'numbound';
						var leftNumRatio = leftMT == 'basetype_ratio';
						var rightNumRatio = rightMT == 'basetype_ratio';
						switch (op)
						{
						case '^':
						case '-':
						case '+':
						case '*':
							if ((leftNumInt || leftNumRatio) && (rightNumInt || rightNumRatio))
								return this._stRatio();
							throw 'MYTODO: bad metatypes for x ' + op + ' y in _binaryOp: ' + leftMT + '/' + rightMT;
						default:
							throw 'MYTODO: unknown binary op in _binaryOp: ' + op;
						}
					},
				_unaryOpType: function(op, unaryMT) { // return the type of the result of the unary op
						var numInt = unaryMT == 'numcycle' || unaryMT == 'numbound';
						var numRatio = unaryMT == 'basetype_ratio';
						switch (op)
						{
						case '-':
							if (numInt || numRatio)
								return this._stRatio();
							throw 'MYTODO: bad metatypes for ' + op + 'x in _unaryOp: ' + unaryMT;
						default:
							throw 'MYTODO: unknown unary op in _unaryOp (' + unaryMT + '): ' + op;
						}
					},
				_calculateType: function(ast) {
						switch (ast.getName())
						{
						case 'RatioLiteral':
							return this._stRatio();
						case 'BinaryOp':
							return this._binaryOpType(
									this._calculateType(ast.toChild('BinLeftValue')).metatype,
									ast.getData().op.chars,
									this._calculateType(ast.toChild('BinRightValue')).metatype
								);
							break;
						case 'UnaryOp':
							return this._unaryOpType(
									ast.getData().op.chars,
									this._calculateType(ast.toChild('UnaValue')).metatype
								);
							break;
						case 'Expression':
							return this._calculateType(ast.toChild('ExpressionHead'));
						}
						throw 'MYTODO: unknown node in calculateType: ' + ast.getName();
					},
				_binaryOpRatio: function(leftR, op, rightR) {
						leftR = ratio.createCopy(leftR);
						switch (op)
						{
						case '^': return leftR.pow(rightR);
						case '-': return leftR.sub(rightR);
						case '+': return leftR.add(rightR);
						case '*': return leftR.mul(rightR);
						}
						throw 'MYTODO: unknown op in binaryOpRatio: ' + op;
					},
				_unaryOpRatio: function(op, unaR) {
						unaR = ratio.createCopy(unaR);
						switch (op)
						{
						case '-': return unaR.neg();
						case '+': return unaR;
						}
						throw 'MYTODO: unknown op in unaryOpRatio: ' + op;
					},
				_calculateRatio: function(ast) {
						switch (ast.getName())
						{
						case 'RatioLiteral':
							return ratio.createCopy(ast.getData().value.val);
						case 'BinaryOp':
							return this._binaryOpRatio(
									this._calculateRatio(ast.toChild('BinLeftValue')),
									ast.getData().op.chars,
									this._calculateRatio(ast.toChild('BinRightValue'))
								);
						case 'UnaryOp':
							return this._unaryOpRatio(
									ast.getData().op.chars,
									this._calculateRatio(ast.toChild('UnaValue'))
								);
						case 'Expression':
							return this._calculateRatio(ast.toChild('ExpressionHead'));
						}
						throw 'MYTODO: unknown node in calculateRatio: ' + ast.getName();
					},
				_validateAndGetRatio: function(ast, needNow) {
						var t = this._calculateType(ast);
						if (t.metatype == 'basetype_ratio')
						{
							var n = this._calculateRatio(ast);
							return n;
						}
						throw 'MYTODO: not sure what to do here, in validateAndGetRatio';
					},
				_define: function(name, templateParams, metatype, metatypeParams, bodyAst) {
						// perform extra injections into statics... but always return the data for
						// the static at `name`
						var o = {
								templateParams: templateParams,
								metatype: metatype,
								metatypeParams: metatypeParams,
								bodyAst: bodyAst,
								simple: false  // simple define's aren't affected by templating
							};

						if (templateParams.length > 0)
							throw 'MYTODO: don\'t know how to do templating yet for ' + name;

						// validate as much as possible
						switch (metatype)
						{
						case 'basetype_big':
						case 'basetype_blob':
						case 'basetype_code':
						case 'basetype_label':
						case 'basetype_ratio':
						case 'basetype_text':
						case 'basetype_ubig':
						case 'union_all':
							o.simple = true;
							break;
						case 'numcycle':
						case 'numbound':
							var n1 = this._validateAndGetRatio(metatypeParams[0], false);
							var n2 = this._validateAndGetRatio(metatypeParams[1], false);
							if (n1 === false || n2 === false)
								throw 'MYTODO: not sure what to do here, in _define, under numcycle/numbound';
							if (n1.cmp(n2) <= 0)
								throw 'MYTODO: invalid ' + metatype + ', first value must be lower than second value';
							o.simple = true;
							o.lower = n1;
							o.upper = n2;
							break;
						case 'basetype_table':
							break;
						case 'static':
							break;
						case 'symbol':
							// MYTODO: confirm metatypeParams are symbols or enums (?)
							// MYTODO: upgrade metatypeParam enums to symbols (?)
							// MYTODO: put all symbols into statics
							// MYTODO: allow conversion between parents <-> this type
							// MYTODO: check for single symbols (`null`) and modify o
							break;
						case 'union':
							break;
						default:
							throw 'internal error: don\'t know how to _define ' + metatype;
						}

						// if the type is simple, and the user has specified templating, then at least
						// warn them that it's a bit silly...
						// ex:   define test#[A, B] basetype_label() end
						if (o.simple && templateParams.length > 0)
							throw 'MYTODO: warning: templating is unnecessary for ' + name;

						return o;
					},
				_postDefine: function(st) {
						//console.log('got: ' + JSON.stringify(st.getData()));
						// MYTODO: validate some more..?
					},
				definePattern: function(regex, createFunc) {
						var my = this;
						statics.addGenerator(regex, function(match) {
								var r = createFunc(match);
								return my._define(match[0], r.templateParams, r.metatype, r.metatypeParams, r.bodyAst);
							}, function(st) {
								my._postDefine(st);
							});
						return this;
					},
				define: function(name, templateParams, metatype, metatypeParams, bodyAst) {
						this._postDefine(
								statics.add(
										name,
										this._define(name, templateParams, metatype, metatypeParams, bodyAst)
									).to(name)
							);
						return this;
					},
				_loadAstDefines: function(ast) {
						// handle a particular DefineBlock tree node
						var my = this;
						function handle_define(def)
						{
							// collect template names
							var tp = [];
							if (def.hasChild('TemplateNameList'))
							{
								def.toChild('TemplateNameList').each(function() {
										tp.push(this.getData().name.chars);
									});
							}

							// collect metatype param ASTs
							var mp = [];
							if (def.hasChild('MetatypeParamList'))
							{
								def.toChild('MetatypeParamList').each(function() {
										mp.push(this);
									});
							}

							// grab body AST
							var body = false;
							if (def.hasChild('DefineBody'))
								body = def.toChild('DefineBody');

							// add definition to type tracker
							var d = def.getData();
							my.define(d.name.chars, tp, d.metatype.chars, mp, body);
						}

						// traverse the AST, looking for DefineBlock's, and making sure that they aren't
						// found anywhere except the start of the program
						var run_time = false;
						function find_defines()
						{
							switch (this.getName())
							{
							case 'DefineBlock':
								if (run_time)
									throw 'MYTODO: error: define happens during run-time';
								handle_define(this);
								return;
							case 'StatementList':
								this.each(find_defines);
								return;
							}
							run_time = true;
							this.each(find_defines);
						}
						ast.each(find_defines);
					},
				loadAst: function(ast) {
						// load the definitions
						this._loadAstDefines(ast);
					},
				injectPatternedTypes: function() {
						var my = this;
						preDefinedPatternedTypes(function(pat, func) {
								my.definePattern(pat, function(match) {
										var r = func(match);
										switch (r.metatype)
										{
										case 'numcycle':
										case 'numbound':
											if (r.constant)
												throw 'MYTODO: don\'t know how to do constants in injectPatternedTypes';
											return {
													templateParams: [],
													metatype: is_cycle ? 'numcycle' : 'numbound',
													metatypeParams: [
															tree.createNode('RatioLiteral', { value: { val: r.numLow } }),
															tree.createNode('RatioLiteral', { value: { val: r.numHigh } })
														],
													bodyAst: false
												};
										}
										throw 'MYTODO: don\'t know how to handle ' + r.metatype + ' inside of injectPatternedTypes';
									})
							});
					}
			};
	};

